ZT Blog
dev

uni-app 深色模式完整指南:从原理到华为白底白字 bug 排查

#uni-app#darkmode#frontend#bugfix#harmonyos

前言

最近在 SimbaSpeaks 项目里踩了一个很经典的坑: 一切开发测试都正常, 但把安装包发到华为手机上, 登录页的输入框里打的字完全看不见. 排查过程意外地复杂, 因为涉及到三层独立的"深色模式"机制互相作用. 把这次踩坑完整记录下来, 也顺手把"如何在 uni-app 里正确开启/关闭深色模式"系统性地讲一遍.

三层架构总览

uni-app 应用里到底有几层 "深色模式" 在起作用? 一开始我也以为是单一机制, 排查后才发现有三层独立生效:

三层架构图

下面逐层拆解.

Layer 1: uni-app 框架 DarkMode (官方机制)

这是 uni-app 官方文档 提供的深色模式接入方案. 它依赖三个东西同时存在:

  1. manifest.json 里开启 darkmode: true
  2. 指定 themeLocation 指向一个 theme.json
  3. theme.json 里分别定义 lightdark 两个变量集合

1.1 各平台分别配置

uni-app 是跨端的, darkmode 需要每个目标平台都显式声明:

{
  "app-plus":    { "darkmode": true, "themeLocation": "theme.json" },
  "app-harmony": { "darkmode": true, "themeLocation": "theme.json" },
  "h5":          { "darkmode": true, "themeLocation": "theme.json" },
  "mp-weixin":   { "darkmode": true, "themeLocation": "theme.json" }
}

1.2 theme.json 结构

{
  "light": {
    "navBgColor": "#f8f8f8",
    "navTxtStyle": "black",
    "bgColor":    "#ffffff"
  },
  "dark": {
    "navBgColor": "#292929",
    "navTxtStyle": "white",
    "bgColor":    "#1f1f1f"
  }
}

然后在 pages.json 里用 @变量名 引用:

{
  "globalStyle": {
    "navigationBarBackgroundColor": "@navBgColor",
    "navigationBarTextStyle": "@navTxtStyle",
    "backgroundColor": "@bgColor"
  }
}

1.3 关键限制 (官方文档)

  • iOS 13+ / Android 10+ 设备才支持
  • 必须云端打包 (HBuilderX 自定义基座都不行)
  • App 端需要先调用 plus.nativeUI.setUIStyle('auto') 才能监听到主题切换

Layer 2: OS 强制 WebView 深色 (这次 bug 的真凶)

这是华为/小米等国产 ROM WebView 内置的"强制网页深色"功能, 跟 uni-app 完全无关:

  • 华为 EMUI/HarmonyOS: 设置 → 显示 → 深色模式 (或电池省电模式) 会自动启用
  • Android 10+: 由 app 主题里的 android:forceDarkAllowed 属性控制

2.1 现象

开启后, WebView 会强行给页面里所有 <input>/<textarea>/<select> 应用深色样式:

input { color: rgba(0, 0, 0, 0.3) !important; }

如果你的页面里 .input 没有显式 color, 浏览器就用这个浅色; 同时 .card 是硬编码 background: #fff 不变, 结果就是 白底浅字 → 看不见.

2.2 跟 Layer 1 的区别

维度 Layer 1 (uni-app) Layer 2 (OS WebView)
触发方 开发者主动配置 OS 自动应用
作用范围 theme.json 定义的变量 所有未显式 color 的元素
是否需要 theme.json
是否影响 input 不会 (除非变量引用到) , 且不可控

这次 bug 完全是 Layer 2 引起的, 因为项目根本没配置 theme.json, Layer 1 实际上是 no-op.

Layer 3: CSS @media prefers-color-scheme

CSS 标准里的媒体查询, 完全由开发者决定怎么用:

/* 默认浅色 */
.card { background: white; color: black; }

/* 系统深色模式下生效 */
@media (prefers-color-scheme: dark) {
  .card { background: #1b1b1b; color: #fff; }
}

兼容性: Chrome 76+ / Safari 12.1+ / Firefox 67+. uni-app 的 App WebView 是 Chrome 内核, 完全兼容.

完整方案: 修复华为白底白字 + 关闭 DarkMode

我们项目最终决定关闭 DarkMode (产品方向暂不支持), 但仍然需要修复华为的强制深色 bug. 完整方案需要三处同时配置:

3.1 Layer 1: 关闭 manifest darkmode

{
  "app-plus": { "darkmode": false, ... },
  "mp-weixin": { "darkmode": false, ... }
}

3.2 Layer 2: 添加 color-scheme meta (H5 入口)

index.html<head> 里加:

<meta name="color-scheme" content="light">

这个 meta 告诉浏览器: "我的页面只支持 light theme, 请不要自动应用深色样式". 这是修复 Layer 2 最干净的办法, 不需要修改每个元素的 CSS.

3.3 Layer 2 + 3: 全局 :root color-scheme

App.vue 的全局 <style> 里加:

:root {
  color-scheme: light;
}

<meta> 等价, 但作为 CSS 标准属性在所有平台编译结果里都生效 (H5/小程序/App WebView).

3.4 完整改动清单

文件 改动 作用层
src/manifest.json app-plus.darkmode: false + mp-weixin.darkmode: false L1
index.html 添加 <meta name="color-scheme" content="light"> L2
src/App.vue 添加 :root { color-scheme: light; } L2 + L3

验证清单

修复后在华为真机上验证:

  • 系统设置: 浅色 / 深色 / 跟随电池 三种模式切换
  • input 输入文字: 始终深色清晰可见
  • 占位符 (placeholder): 颜色不变
  • 验证码图片: 正常显示
  • 其他平台 (iOS / 小米 / 微信小程序) 行为一致

Chrome DevTools 可以用 Rendering 面板里的 Emulate CSS prefers-color-scheme: dark 模拟深色模式, 快速验证 L3 不会被意外触发.

反过来: 如果要开启深色模式怎么办?

对于从零开始的项目, 建议流程是:

  1. 先写完整的 L3 (@media (prefers-color-scheme: dark) 适配), 因为这是最底层的, 不依赖任何框架.
  2. 再加 L1 (uni-app manifest + theme.json) 处理原生组件 (navigationBar, tabBar).
  3. 测试 L2 在华为等设备上的表现, 必要时再加 color-scheme: light dark; 让浏览器允许深色.

如果跳过了 L3 直接做 L1, 就会出现我们这次遇到的 "页面硬编码浅色, 但 WebView 自动深色" 的撕裂感.

总结

  • 深色模式不是单一开关, uni-app 里至少有三层互相独立的机制
  • 华为白底白字 bug 的真凶是 Layer 2 (OS WebView 强制深色), 不是 Layer 1
  • color-scheme 是修复 Layer 2 最干净的方式, 比给每个 input 加 !important color 优雅得多
  • 关掉 DarkMode 时也要每个目标平台都显式声明, 避免未来 build 时漏掉某个平台

参考: